﻿// -----------------------------------------------------------------------
// <copyright company="www.3WayWebdesign.de">
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </copyright>
// 
// <author>Michael Morbach</author>
// <email>Michael@3waywebdesign.de</email>
// <web>http://www.3waywebdesign.de</web>
// ----------------------------------------------------------------------

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Media.Animation;

namespace Tww.MAF.MAFControlSuite.Controls.Shared
{
    public class ScrollableTabPanel : Panel, IScrollInfo, INotifyPropertyChanged
    {
        #region  - Felder (öffentlich) -

        /// <summary>
        /// The animation time span property
        /// </summary>
        public static readonly DependencyProperty AnimationTimeSpanProperty =
            DependencyProperty.Register("AnimationTimeSpanProperty", typeof(TimeSpan), typeof(ScrollableTabPanel),
                new FrameworkPropertyMetadata(new TimeSpan(0, 0, 0, 0, 100),
                    FrameworkPropertyMetadataOptions.AffectsRender));

        /// <summary>
        /// The line scroll pixel count property
        /// </summary>
        public static readonly DependencyProperty LineScrollPixelCountProperty =
            DependencyProperty.Register("LineScrollPixelCount", typeof(int), typeof(ScrollableTabPanel),
                new FrameworkPropertyMetadata(15, FrameworkPropertyMetadataOptions.AffectsRender));

        /// <summary>
        /// The right overflow margin property
        /// </summary>
        public static readonly DependencyProperty RightOverflowMarginProperty =
            DependencyProperty.Register("RightOverflowMargin", typeof(int), typeof(ScrollableTabPanel),
                new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.AffectsRender));

        /// <summary>
        /// The show transparent overflow property
        /// </summary>
        public static readonly DependencyProperty ShowTransparentOverflowProperty =
            DependencyProperty.Register("ShowTransparentOverflow", typeof(bool), typeof(ScrollableTabPanel),
                new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.AffectsRender));

        #endregion

        #region  - Felder (privat) -

        /// <summary>
        /// The gradient stops transparent left
        /// </summary>
        private static readonly GradientStopCollection GradientStopsTransparentLeft = new GradientStopCollection
        {
            new GradientStop(Colors.Transparent, 0),
            new GradientStop(Colors.Black, 0.8)
        };

        /// <summary>
        /// The gradient stops transparent left and right
        /// </summary>
        private static readonly GradientStopCollection GradientStopsTransparentLeftAndRight = new GradientStopCollection
        {
            new GradientStop(Colors.Transparent, 0.0),
            new GradientStop(Colors.Black, 0.2),
            new GradientStop(Colors.Black, 0.8),
            new GradientStop(Colors.Transparent, 1.0)
        };

        /// <summary>
        /// The gradient stops transparent right
        /// </summary>
        private static readonly GradientStopCollection GradientStopsTransparentRight = new GradientStopCollection
        {
            new GradientStop(Colors.Black, 0.8),
            new GradientStop(Colors.Transparent, 1)
        };

        /// <summary>
        ///     This will apply the present scroll-position resp. -offset.
        /// </summary>
        private readonly TranslateTransform scrollTransform = new TranslateTransform();

        private bool canScrollHorizontally = true;
        private Size controlExtensionSize = new Size(0, 0);
        private Size controlViewportSize = new Size(0, 0);
        private Vector offset;
        private ScrollViewer owningScrollViewer;

        #endregion

        #region  - Konstruktoren -

        /// <summary>
        /// Initializes a new instance of the <see cref="ScrollableTabPanel"/> class.
        /// </summary>
        public ScrollableTabPanel()
        {
            RenderTransform = scrollTransform;
            SizeChanged += ScrollableTabPanel_SizeChanged;
        }

        #endregion

        #region  - Indexer und Eigenschaften (öffentlich) -

        /// <summary>
        /// Gets or sets a value indicating whether [show transparent overflow].
        /// </summary>
        /// <value>
        /// <c>true</c> if [show transparent overflow]; otherwise, <c>false</c>.
        /// </value>
        public bool ShowTransparentOverflow
        {
            get { return (bool)GetValue(ShowTransparentOverflowProperty); }
            set { SetValue(ShowTransparentOverflowProperty, value); }
        }

        /// <summary>
        ///     Retrieves the overall resp. internal/inner size of the control/panel.
        /// </summary>
        /// <remarks>The setter is private to the class.</remarks>
        public Size Extent
        {
            get { return controlExtensionSize; }
            private set { controlExtensionSize = value; }
        }

        /// <summary>
        ///     Retrieves the outer resp. visible size of the control/panel.
        /// </summary>
        /// <remarks>The setter is private to the class.</remarks>
        public Size Viewport
        {
            get { return controlViewportSize; }
            private set { controlViewportSize = value; }
        }


        /// <summary>
        ///     Retrieves whether the panel's scroll-position is on the far left (i.e. cannot scroll further to the left).
        /// </summary>
        public bool IsOnFarLeft
        {
            get { return Math.Abs(HorizontalOffset) < 0; }
        }

        /// <summary>
        ///     Retrieves whether the panel's scroll-position is on the far right (i.e. cannot scroll further to the right).
        /// </summary>
        public bool IsOnFarRight
        {
            get { return Math.Abs((HorizontalOffset + Viewport.Width) - ExtentWidth) < 0; }
        }

        /// <summary>
        ///     Retrieves whether the panel's viewport is larger than the control's extent, meaning there is hidden content
        ///     that the user would have to scroll for in order to see it.
        /// </summary>
        public bool CanScroll
        {
            get { return ExtentWidth > Viewport.Width; }
        }

        /// <summary>
        ///     Retrieves whether the panel's scroll-position is NOT on the far left (i.e. can scroll to the left).
        /// </summary>
        public bool CanScrollLeft
        {
            get { return CanScroll && !IsOnFarLeft; }
        }

        /// <summary>
        ///     Retrieves whether the panel's scroll-position is NOT on the far right (i.e. can scroll to the right).
        /// </summary>
        public bool CanScrollRight
        {
            get { return CanScroll && !IsOnFarRight; }
        }

        /// <summary>
        ///     Sets or retrieves the Margin that will be applied to the rightmost item in the panel;
        ///     This allows for the item applying a negative margin, i.e. when selected.
        ///     If set to a value other than zero (being the default), the control will add the value
        ///     specified here to the item's right extent.
        /// </summary>
        public int RightOverflowMargin
        {
            get { return (int)GetValue(RightOverflowMarginProperty); }
            set { SetValue(RightOverflowMarginProperty, value); }
        }

        /// <summary>
        ///     Sets or retrieves the the duration (default: 100ms) for the panel's transition-animation that is
        ///     started when an item is selected (scroll from the previously selected item to the
        ///     presently selected one).
        /// </summary>
        public TimeSpan AnimationTimeSpan
        {
            get { return (TimeSpan)GetValue(AnimationTimeSpanProperty); }
            set { SetValue(AnimationTimeSpanProperty, value); }
        }

        /// <summary>
        ///     Sets or retrieves the count of pixels to scroll by when the LineLeft or LineRight methods
        ///     are called (default: 15px).
        /// </summary>
        public int LineScrollPixelCount
        {
            get { return (int)GetValue(LineScrollPixelCountProperty); }
            set { SetValue(LineScrollPixelCountProperty, value); }
        }

        #endregion

        #region  - INotifyPropertyChanged Members -

        /// <summary>
        ///     Raised when a property on this object has a new value.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region  - IScrollInfo Members -

        /// <summary>
        ///     Sets or retrieves whether the control is allowed to scroll horizontally.
        /// </summary>
        public bool CanHorizontallyScroll
        {
            get { return canScrollHorizontally; }
            set { canScrollHorizontally = value; }
        }

        /// <summary>
        ///     Sets or retrieves whether the control is allowed to scroll vertically.
        /// </summary>
        /// <remarks>
        ///     This is DISABLED for the control! Due to the internal plumbing of the ScrollViewer
        ///     control, this property needs to be accessible without an exception being thrown;
        ///     however, setting this property will do plain nothing.
        /// </remarks>
        public bool CanVerticallyScroll
        {
            //We'll never be able to vertically scroll.
            get { return false; }
            set { }
        }

        /// <summary>
        ///     Retrieves the height of the control; since no vertical scrolling has been
        ///     implemented, this will return the same value at all times.
        /// </summary>
        public double ExtentHeight
        {
            get { return Extent.Height; }
        }

        /// <summary>
        ///     Retrieves the overall width of the content hosted in the panel (i.e., the width
        ///     measured between [far left of the scrollable portion] and [far right of the scrollable portion].
        /// </summary>
        public double ExtentWidth
        {
            get { return Extent.Width; }
        }

        /// <summary>
        ///     Retrieves the current horizontal scroll offset.
        /// </summary>
        /// <remarks>The setter is private to the class.</remarks>
        public double HorizontalOffset
        {
            get { return offset.X; }
            private set { offset.X = value; }
        }

        /// <summary>
        ///     Increments the vertical offset.
        /// </summary>
        /// <remarks>This is unsupported.</remarks>
        public void LineDown()
        {
            throw new InvalidOperationException();
        }

        /// <summary>
        ///     Decrements the horizontal offset by the amount specified in the <see cref="LineScrollPixelCount" /> property.
        /// </summary>
        public void LineLeft()
        {
            SetHorizontalOffset(HorizontalOffset - LineScrollPixelCount);
        }

        /// <summary>
        ///     Increments the horizontal offset by the amount specified in the <see cref="LineScrollPixelCount" /> property.
        /// </summary>
        public void LineRight()
        {
            SetHorizontalOffset(HorizontalOffset + LineScrollPixelCount);
        }

        /// <summary>
        ///     Decrements the vertical offset.
        /// </summary>
        /// <remarks>This is unsupported.</remarks>
        public void LineUp()
        {
            throw new InvalidOperationException();
        }

        /// <summary>
        ///     Scrolls a child of the panel (Visual) into view.
        /// </summary>
        public Rect MakeVisible(Visual visual, Rect rectangle)
        {
            if (rectangle.IsEmpty || visual == null
                || Equals(visual, this) || !IsAncestorOf(visual))
            {
                return Rect.Empty;
            }

            double dblOffsetX = 0;
            UIElement uieControlToMakeVisible = null;
            for (int i = 0; i < InternalChildren.Count; i++)
            {
                if (Equals(InternalChildren[i], visual))
                {
                    uieControlToMakeVisible = InternalChildren[i];
                    dblOffsetX = GetLeftEdge(InternalChildren[i]);
                    break;
                }
            }

            //Set the offset only if the desired element is not already completely visible.
            if (uieControlToMakeVisible != null)
            {
                if (Equals(uieControlToMakeVisible, InternalChildren[0]))
                    //If the first child has been selected, go to the very beginning of the scrollable area
                    dblOffsetX = 0;
                else if (Equals(uieControlToMakeVisible, InternalChildren[InternalChildren.Count - 1]))
                    //If the last child has been selected, go to the very end of the scrollable area
                    dblOffsetX = ExtentWidth - Viewport.Width;
                else
                    dblOffsetX = CalculateNewScrollOffset(
                        HorizontalOffset,
                        HorizontalOffset + Viewport.Width,
                        dblOffsetX,
                        dblOffsetX + uieControlToMakeVisible.DesiredSize.Width
                        );

                SetHorizontalOffset(dblOffsetX);
                rectangle = new Rect(HorizontalOffset, 0, uieControlToMakeVisible.DesiredSize.Width, Viewport.Height);
            }

            return rectangle;
        }

        public void MouseWheelDown()
        {
            //We won't be responding to the mouse-wheel.
        }

        public void MouseWheelLeft()
        {
            //We won't be responding to the mouse-wheel.
        }

        public void MouseWheelRight()
        {
            //We won't be responding to the mouse-wheel.
        }

        public void MouseWheelUp()
        {
            //We won't be responding to the mouse-wheel.
        }

        public void PageDown()
        {
            //We won't be responding to vertical paging.
        }

        public void PageLeft()
        {
            //We won't be responding to horizontal paging.
        }

        public void PageRight()
        {
            //We won't be responding to horizontal paging.
        }

        public void PageUp()
        {
            //We won't be responding to vertical paging.
        }

        /// <summary>
        ///     Sets or retrieves the ScrollViewer control that hosts the panel.
        /// </summary>
        public ScrollViewer ScrollOwner
        {
            get { return owningScrollViewer; }
            set
            {
                if (ScrollOwner != null)
                {
                    ScrollOwner.Loaded -= ScrollOwner_Loaded;
                }
                owningScrollViewer = value;
                if (ScrollOwner != null)
                {
                    ScrollOwner.Loaded += ScrollOwner_Loaded;
                }
            }
        }

        public void SetHorizontalOffset(double horizontalOffset)
        {
            //Remove all OpacityMasks while scrolling.
            RemoveOpacityMasks();

            //Assure that the horizontal offset always contains a valid value
            HorizontalOffset = Math.Max(0, Math.Min(ExtentWidth - Viewport.Width, Math.Max(0, horizontalOffset)));

            if (ScrollOwner != null) ScrollOwner.InvalidateScrollInfo();

            //If you don't want the animation, you would replace all the code further below (up to but not including)
            //the call to InvalidateMeasure() with the following line:
            //_ttScrollTransform.X = (-this.HorizontalOffset);

            //Animate the new offset
            var daScrollAnimation =
                new DoubleAnimation(
                    scrollTransform.X,
                    (-HorizontalOffset),
                    new Duration(AnimationTimeSpan),
                    FillBehavior.HoldEnd
                    ) { AccelerationRatio = 0.5, DecelerationRatio = 0.5 };

            //Note that, depending on distance between the original and the target scroll-position and
            //the duration of the animation, the  acceleration and deceleration effects might be more
            //or less unnoticeable at runtime.

            //The childrens' OpacityMask can only be set reliably after the scroll-animation
            //has finished its work, so attach to the animation's Completed event where the
            //masks will be re-created.
            daScrollAnimation.Completed += daScrollAnimation_Completed;

            scrollTransform.BeginAnimation(
                TranslateTransform.XProperty,
                daScrollAnimation,
                HandoffBehavior.Compose);

            InvalidateMeasure();
        }

        public void SetVerticalOffset(double verticalOffset)
        {
            throw new InvalidOperationException();
        }

        public double VerticalOffset
        {
            get { return 0; }
        }

        public double ViewportHeight
        {
            get { return Viewport.Height; }
        }

        public double ViewportWidth
        {
            get { return Viewport.Width; }
        }

        #endregion

        #region  - Methoden (öffentlich) -

        /// <summary>
        ///     Determines whether the passed child control is only partially visible
        ///     (i.e. whether part of it is outside of the Viewport).
        /// </summary>
        /// <param name="uieChild">The child control to be tested.</param>
        /// <returns>
        ///     True if part of the control is further to the left or right of the
        ///     Viewport, False otherwise.
        /// </returns>
        public bool IsPartlyVisible(UIElement uieChild)
        {
            Rect rctIntersect = GetIntersectionRectangle(uieChild);
            return (!(rctIntersect == Rect.Empty));
        }

        /// <summary>
        ///     Determines the visible part of the passed child control,
        ///     measured between 0 (completely invisible) and 1 (completely visible),
        ///     that is overflowing into the right invisible portion of the panel.
        /// </summary>
        /// <param name="uiChild">The child control to be tested.</param>
        /// <returns>
        ///     <para>
        ///         A number between 0 (the control is completely invisible resp. outside of
        ///         the Viewport) and 1 (the control is completely visible).
        ///     </para>
        ///     <para>
        ///         All values between 0 and 1 indicate the part that is visible
        ///         (i.e. 0.4 would mean that 40% of the control is visible, the remaining
        ///         60% will overflow into the right invisible portion of the panel.
        ///     </para>
        /// </returns>
        public double PartlyVisiblePortion_OverflowToRight(UIElement uiChild)
        {
            Rect rctIntersect = GetIntersectionRectangle(uiChild);
            double visiblePortion = 1;
            if (
                !(rctIntersect == Rect.Empty)
                &&
                CanScrollRight
                &&
                rctIntersect.Width < uiChild.DesiredSize.Width
                &&
                rctIntersect.X > 0
                )
                visiblePortion = rctIntersect.Width / uiChild.DesiredSize.Width;

            return visiblePortion;
        }

        /// <summary>
        ///     Determines the visible part of the passed child control,
        ///     measured between 0 (completely invisible) and 1 (completely visible),
        ///     that is overflowing into the left invisible portion of the panel.
        /// </summary>
        /// <param name="uiChild">The child control to be tested.</param>
        /// <returns>
        ///     <para>
        ///         A number between 0 (the control is completely invisible resp. outside of
        ///         the Viewport) and 1 (the control is completely visible).
        ///     </para>
        ///     <para>
        ///         All values between 0 and 1 indicate the part that is visible
        ///         (i.e. 0.4 would mean that 40% of the control is visible, the remaining
        ///         60% will overflow into the left invisible portion of the panel.
        ///     </para>
        /// </returns>
        public double PartlyVisiblePortion_OverflowToLeft(UIElement uiChild)
        {
            Rect intersect = GetIntersectionRectangle(uiChild);
            double visiblePortion = 1;
            if (
                !(intersect == Rect.Empty)
                &&
                CanScrollLeft
                &&
                intersect.Width < uiChild.DesiredSize.Width
                &&
                intersect.X.Equals(0)
                )
                visiblePortion = intersect.Width / uiChild.DesiredSize.Width;

            return visiblePortion;
        }

        #endregion

        #region  - Methoden (privat) -

        /// <summary>
        ///     Calculates the HorizontalOffset for a given child-control, based on a desired value.
        /// </summary>
        /// <param name="viewportLeft">The left offset of the Viewport.</param>
        /// <param name="viewportRight">The right offset of the Viewport.</param>
        /// <param name="childLeft">The left offset of the control in question.</param>
        /// <param name="childRight">The right offset of the control in question.</param>
        /// <returns></returns>
        private static double CalculateNewScrollOffset(
            double viewportLeft,
            double viewportRight,
            double childLeft,
            double childRight
            )
        {
            //Retrieve basic information about the position of the Viewport within the Extent of the control.
            bool fIsFurtherToLeft = (childLeft < viewportLeft) && (childRight < viewportRight);
            bool fIsFurtherToRight = (childRight > viewportRight) && (childLeft > viewportLeft);
            bool fIsWiderThanViewport = (childRight - childLeft) > (viewportRight - viewportLeft);

            if (!fIsFurtherToRight && !fIsFurtherToLeft)
                //Don't change anything - the Viewport is completely visible (inside the Extent's bounds)
                return viewportLeft;

            if (fIsFurtherToLeft && !fIsWiderThanViewport)
                //The child is to be placed with its left edge equal to the left edge of the Viewport's present offset.
                return childLeft;

            //The child is to be placed with its right edge equal to the right edge of the Viewport's present offset.
            return (childRight - (viewportRight - viewportLeft));
        }

        /// <summary>
        ///     This is the 1st pass of the layout process. Here, the Extent's size is being determined.
        /// </summary>
        /// <param name="availableSize">The Viewport's rectangle, as obtained after the 1st pass (MeasureOverride).</param>
        /// <returns>The Viewport's final size.</returns>
        protected override Size MeasureOverride(Size availableSize)
        {
            //The default size will not reflect any width (i.e., no children) and always the default height.
            var resultSize = new Size(0, availableSize.Height);

            //Loop through all child controls ...
            foreach (UIElement uiChild in InternalChildren)
            {
                //... retrieve the desired size of the control ...
                uiChild.Measure(availableSize);
                //... and pass this on to the size we need for the Extent
                resultSize.Width += uiChild.DesiredSize.Width;
            }

            UpdateMembers(resultSize, availableSize);

            double dblNewWidth = double.IsPositiveInfinity(availableSize.Width)
                ? resultSize.Width
                : availableSize.Width;

            resultSize.Width = dblNewWidth;
            return resultSize;
        }

        /// <summary>
        ///     This is the 2nd pass of the layout process, where child controls are
        ///     being arranged within the panel.
        /// </summary>
        /// <param name="finalSize">The Viewport's rectangle, as obtained after the 1st pass (MeasureOverride).</param>
        /// <returns>The Viewport's final size.</returns>
        protected override Size ArrangeOverride(Size finalSize)
        {
            if (InternalChildren == null || InternalChildren.Count < 1)
                return finalSize;

            double totalWidth = 0;
            foreach (UIElement uieChild in InternalChildren)
            {
                double width = uieChild.DesiredSize.Width;
                uieChild.Arrange(new Rect(totalWidth, 0, width, uieChild.DesiredSize.Height));
                totalWidth += width;
            }

            return finalSize;
        }

        /// <summary>
        ///     Wird aufgerufen, wenn die <see cref="T:System.Windows.Media.VisualCollection" /> eines visuellen Objekts geändert
        ///     wurde.
        /// </summary>
        /// <param name="visualAdded">Das der Auflistung hinzugefügte <see cref="T:System.Windows.Media.Visual" />-Objekt.</param>
        /// <param name="visualRemoved">Das aus der Auflistung entfernte <see cref="T:System.Windows.Media.Visual" />-Objekt.</param>
        protected override void OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved)
        {
            base.OnVisualChildrenChanged(visualAdded, visualRemoved);
            UpdateOpacityMasks();
        }

        /// <summary>
        ///     Unterstützt das Layoutverhalten beim Ändern der Größe eines untergeordneten Elements.
        /// </summary>
        /// <param name="child">Das untergeordnete Element, dessen Größe geändert wird.</param>
        protected override void OnChildDesiredSizeChanged(UIElement child)
        {
            base.OnChildDesiredSizeChanged(child);
            UpdateOpacityMasks();
        }

        /// <summary>
        ///     Compares the present sizes (Extent/Viewport) against the local values
        ///     and updates them, if required.
        /// </summary>
        private void UpdateMembers(Size szExtent, Size szViewportSize)
        {
            if (szExtent != Extent)
            {
                //The Extent of the control has changed.
                Extent = szExtent;
                if (ScrollOwner != null) ScrollOwner.InvalidateScrollInfo();
            }

            if (szViewportSize != Viewport)
            {
                //The Viewport of the panel has changed.
                Viewport = szViewportSize;
                if (ScrollOwner != null)
                    ScrollOwner.InvalidateScrollInfo();
            }

            //Prevent from getting off to the right
            if (HorizontalOffset + Viewport.Width + RightOverflowMargin > ExtentWidth)
                SetHorizontalOffset(HorizontalOffset + Viewport.Width + RightOverflowMargin);

            //Notify UI-subscribers
            NotifyPropertyChanged("CanScroll");
            NotifyPropertyChanged("CanScrollLeft");
            NotifyPropertyChanged("CanScrollRight");
        }

        /// <summary>
        ///     Returns the left position of the requested child (in Viewport-coordinates).
        /// </summary>
        /// <param name="uieChild">The child to retrieve the position for.</param>
        private double GetLeftEdge(UIElement uieChild)
        {
            double totalWidth = 0;

            //Loop through all child controls, summing up their required width
            foreach (UIElement uie in InternalChildren)
            {
                //The width of the current child control
                double width = uie.DesiredSize.Width;

                if (uieChild != null && Equals(uieChild, uie))
                    //The current child control is the one in question, so disregard its width
                    //and return the total width required for all controls further to the left,
                    //equaling the left edge of the requested child control.
                    return totalWidth;

                //Sum up the overall width while the child control in question hasn't been hit.
                totalWidth += width;
            }

            //This shouldn't really be hit as the requested control should've been found beforehand.
            return totalWidth;
        }

        /// <summary>
        ///     Returns the currently rendered rectangle that makes up the Viewport.
        /// </summary>
        private Rect GetScrollViewerRectangle()
        {
            return new Rect(new Point(0, 0), ScrollOwner.RenderSize);
        }

        /// <summary>
        ///     Returns the rectangle that defines the outer bounds of a child control.
        /// </summary>
        /// <param name="uiChild">The child/control for which to return the bounding rectangle.</param>
        private Rect GetChildRectangle(UIElement uiChild)
        {
            //Retrieve the position of the requested child inside the ScrollViewer control
            GeneralTransform childTransform = uiChild.TransformToAncestor(ScrollOwner);
            return childTransform.TransformBounds(new Rect(new Point(0, 0), uiChild.RenderSize));
        }

        /// <summary>
        ///     Returns a Rectangle that contains the intersection between the ScrollViewer's
        ///     and the passed child control's boundaries, that is, the portion of the child control
        ///     which is currently visibile within the ScrollViewer's Viewport.
        /// </summary>
        /// <param name="uiChild">The child for which to retrieve Rectangle.</param>
        /// <returns></returns>
        private Rect GetIntersectionRectangle(UIElement uiChild)
        {
            //Retrieve the ScrollViewer's rectangle
            Rect rctScrollViewerRectangle = GetScrollViewerRectangle();
            Rect rctChildRect = GetChildRectangle(uiChild);

            //Return the area/rectangle in which the requested child and the ScrollViewer control's Viewport intersect.
            return Rect.Intersect(rctScrollViewerRectangle, rctChildRect);
        }

        /// <summary>
        ///     Will remove the OpacityMask for all child controls.
        /// </summary>
        private void RemoveOpacityMasks()
        {
            foreach (UIElement uiChild in Children)
            {
                RemoveOpacityMask(uiChild);
            }
        }

        /// <summary>
        ///     Will remove the OpacityMask for all child controls.
        /// </summary>
        private void RemoveOpacityMask(UIElement uiChild)
        {
            uiChild.OpacityMask = null;
        }

        /// <summary>
        ///     Will check all child controls and set their OpacityMasks.
        /// </summary>
        private void UpdateOpacityMasks()
        {
            foreach (UIElement uiChild in Children)
            {
                UpdateOpacityMask(uiChild);
            }
        }

        /// <summary>
        ///     Takes the given child control and checks as to whether the control is completely
        ///     visible (in the Viewport). If not (i.e. if it's only partially visible), an OpacityMask
        ///     will be applied so that it fades out into nothingness.
        /// </summary>
        private void UpdateOpacityMask(UIElement uiChild)
        {
            if (uiChild == null) return;

            //Retrieve the ScrollViewer's rectangle
            Rect rctScrollViewerRectangle = GetScrollViewerRectangle();
            if (rctScrollViewerRectangle == Rect.Empty) return;

            //Retrieve the child control's rectangle
            Rect rctChildRect = GetChildRectangle(uiChild);

            if (rctScrollViewerRectangle.Contains(rctChildRect))
                //This child is completely visible, so dump the OpacityMask.
                uiChild.OpacityMask = null;
            else
            {
                double partlyVisiblePortionOverflowToLeft = PartlyVisiblePortion_OverflowToLeft(uiChild);
                double partlyVisiblePortionOverflowToRight = PartlyVisiblePortion_OverflowToRight(uiChild);

                if (partlyVisiblePortionOverflowToLeft < 1 && partlyVisiblePortionOverflowToRight < 1 && ShowTransparentOverflow)
                    uiChild.OpacityMask = new LinearGradientBrush(GradientStopsTransparentLeftAndRight,
                        new Point(0, 0),
                        new Point(1, 0));
                else if (partlyVisiblePortionOverflowToLeft < 1 && ShowTransparentOverflow)
                    //A part of the child (to the left) remains invisible, so fade out to the left.
                    uiChild.OpacityMask = new LinearGradientBrush(
                        GradientStopsTransparentLeft,
                        new Point(1 - partlyVisiblePortionOverflowToLeft, 0),
                        new Point(1, 0)
                        );
                else if (partlyVisiblePortionOverflowToRight < 1 && ShowTransparentOverflow)
                    //A part of the child (to the right) remains invisible, so fade out to the right.
                    uiChild.OpacityMask = new LinearGradientBrush(
                        GradientStopsTransparentRight,
                        new Point(0, 0),
                        new Point(partlyVisiblePortionOverflowToRight, 0)
                        );
                else
                    //This child is completely visible, so dump the OpacityMask.
                    //Actually, this part should never be reached as, in this case, the very first
                    //checkup should've resulted in the child-rect being completely contained in
                    //the SV's rect; Well, I'll leave this here anyhow (just to be save).
                    uiChild.OpacityMask = null;
            }
        }

        /// <summary>
        ///     Called from within this class whenever subscribers (i.e. bindings) are to be notified of a property-change
        /// </summary>
        /// <param name="strPropertyName">The name of the property that has changed.</param>
        private void NotifyPropertyChanged(String strPropertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(strPropertyName));
            }
        }

        /// <summary>
        ///     Fired when the ScrollViewer is initially loaded/displayed.
        ///     Required in order to initially setup the childrens' OpacityMasks.
        /// </summary>
        private void ScrollOwner_Loaded(object sender, RoutedEventArgs e)
        {
            UpdateOpacityMasks();
        }

        /// <summary>
        ///     Fired when the scroll-animation has finished its work, that is, at the
        ///     point in time when the ScrollViewerer has reached its final scroll-position
        ///     resp. offset, which is when the childrens' OpacityMasks can be updated.
        /// </summary>
        private void daScrollAnimation_Completed(object sender, EventArgs e)
        {
            UpdateOpacityMasks();

            //This is required in order to update the TabItems' FocusVisual
            foreach (UIElement uieChild in InternalChildren)
                uieChild.InvalidateArrange();
        }

        private void ScrollableTabPanel_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            UpdateOpacityMasks();
        }

        #endregion
    }
}